﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace GameLibrary.AI.EA
{
    using Unit;

    /// <summary>
    /// Find  new template through Evolution Algorithms.
    /// </summary>
    [Serializable]
    public class GetNewTemplate
    {
        #region individual

        /// <summary>
        /// store gene information of new template
        /// </summary>
        private class Individual
        {
            public List<uint> AttributeValueList; 
            public float Fitness;
            public Template Template;
            public Individual(Template t)
            {
                this.AttributeValueList = new List<uint>() { t.HP, t.AP, t.Attack, t.Defence, t.AttackRange };
                this.Fitness = float.NaN;
                this.Template = new Template(
                                         this.AttributeValueList[0],
                                         0,
                                         this.AttributeValueList[1],
                                         this.AttributeValueList[2],
                                         this.AttributeValueList[3],
                                         this.AttributeValueList[4],
                                         1,
                                         UnitActionFlags.Default);     
            }
            public Individual(Individual individual)
            {
                this.AttributeValueList = new List<uint>(individual.AttributeValueList);
                this.Fitness = individual.Fitness;
                this.Template = new Template(
                                 this.AttributeValueList[0],
                                 0,
                                 this.AttributeValueList[1],
                                 this.AttributeValueList[2],
                                 this.AttributeValueList[3],
                                 this.AttributeValueList[4],
                                 1,
                                 UnitActionFlags.Default);
            }
        }

        #endregion

        #region variables

        private const int totalPositions = 5;//5 - hp,ap,att,def,attrange
        private List<Individual> Population;
        private int PopulationSize;
        private int ChildSize;
        private int TotalTurns;
        private UnitInfo TargetUnit;
        private float TotalFitness;
        private float BudgetLimit;
        private UnitInfo CurrentUnit;

        #endregion
        
        #region constructor

        public GetNewTemplate(
            Template currentTemplate,
            Template targetTemplate,
            float budgetLimit,
            int populationSize = 100,
            int childSize = 500,
            int totalTurns = 500
            )
        {
            this.PopulationSize = populationSize;
            this.Population = new List<Individual>(populationSize+childSize);
            this.ChildSize = childSize;
            this.TotalTurns = totalTurns;

            Unit unit = new Unit(Position.Zero, 0, targetTemplate);
            unit.NextTurn();
            this.TargetUnit = unit.Info;

            unit = new Unit(Position.Zero, 0, currentTemplate);
            unit.NextTurn();
            this.CurrentUnit = unit.Info;

            this.BudgetLimit = budgetLimit;
        }

        #endregion

        #region initial

        private void InitialPopulation()
        {
            for (int i = 0; i < this.PopulationSize; i++)
            {
                Individual individual = GetValidIndividual();

                if (individual.Template.Cost <= this.BudgetLimit) 
                {
                    this.FindFitness(ref individual);
                    this.Population.Add(individual);
                }
            }
        }

        private Individual GetValidIndividual()
        {
            Individual individual = new Individual(this.CurrentUnit.Template);
            this.Mutation(ref individual); // make modification
            return individual;
        }

        #endregion

        #region mutation

        /// <summary>
        /// Add or substract 1 from one of a's list items.
        /// Do not substract if position selected is already 0;
        /// </summary>
        /// <param name="individual"></param>
        private void Mutation(ref Individual individual)
        {
            int position = GMHelper.Rand.Next(totalPositions); // select a position to mutate

            if (individual.AttributeValueList[position] <= 0)
                individual.AttributeValueList[position]++;
            else
            {
                double probility = GMHelper.Rand.NextDouble();

                if (probility > .5)
                    individual.AttributeValueList[position]++;
                else
                    individual.AttributeValueList[position]--;
            }
        }

        #endregion

        #region select

        /// <summary>
        /// Select an individual from existing population by their fitness / total fitness ratio.
        /// </summary>
        /// <returns></returns>
        private Individual PropotionalSelect()
        {
            float targetValue = (float) GMHelper.Rand.NextDouble();
            float total = 0;
            Individual selectedNode = null;
            foreach (var individual in this.Population)
            {
                total += individual.Fitness / this.TotalFitness;
                if (total >= targetValue)
                {
                    selectedNode = individual;
                    break;
                }
            }
            return selectedNode;
        }

        #endregion

        #region crossover

        /// <summary>
        /// Change value of one position of a.list to value of same position of b's list
        /// </summary>
        /// <param name="nodeA"></param>
        /// <param name="individualB"></param>
        private void Crossover(ref Individual individualA, Individual individualB)
        {
            int position = GMHelper.Rand.Next(totalPositions);
            uint tmp = individualA.AttributeValueList[position];
            individualA.AttributeValueList[position] = individualB.AttributeValueList[position];
        }
        
        #endregion

        #region find fitness
        
        /// <summary>
        /// find fitness by fighting new template of node with target template.
        /// Calculate method:
        ///     1. attack target 5 times, then defence target 5 times
        ///         get result 1stResult = total winning times / 10
        ///     2. defence 5 times, then attact 5 times
        ///         get result 2ndResult = total winning times / 10
        ///     3. get fitness = (1stRestlut + 2ndResult)/2   
        /// </summary>
        /// <param name="individual"></param>
        private void FindFitness(ref Individual individual)
        {
            Unit unit = new Unit(Position.Zero, 0, individual.Template);
            unit.NextTurn();
            UnitInfo info = unit.Info;

            int _1stResult = this.Attack(5, info);
            if (_1stResult == 5)
                _1stResult += this.Defence(5, info);
            _1stResult/=10;

            info = unit.Info;
            int _2ndResult = this.Defence(5, info);
            if (_2ndResult == 5)
                _2ndResult += this.Attack(5, info);
            _2ndResult /= 10;

            individual.Fitness = (_1stResult + _2ndResult) / 2;
        }

        /// <summary>
        ///  a attack b times times.
        ///  return how many time a wins
        /// </summary>
        /// <param name="times"></param>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        private int Attack(int times, UnitInfo attacker)
        {
            int winningTimes = 0;

            for (int i = 0; i < times; i++)
            {
                float aHP,tHP;
                AI.FightSimulator.GetResult(attacker, this.TargetUnit, out aHP, out tHP);
                if (aHP > 0)
                {
                    winningTimes++;
                    attacker.Current.HP = aHP;
                }
                else 
                    break;
            }
            return winningTimes;
        }

        private int Defence(int times, UnitInfo defender)
        {
            int winningTimes = 0;
            for (int i = 0; i < times; i++)
            {
                float dhp, thp;
                AI.FightSimulator.GetResult(this.TargetUnit, defender, out thp, out dhp);
                if (dhp > 0)
                {
                    defender.Current.HP = dhp;
                    winningTimes++;
                }
                else
                    break;
            }
            return winningTimes;
        }

        #endregion

        #region next turn

        /// <summary>
        /// Get next generation of children.
        /// Merge with current population.
        /// Reduce total population to population size
        /// </summary>
        private void NextTurn()
        {
            this.TotalFitness = 0;
            this.Population.ForEach(n => this.TotalFitness += n.Fitness);

            List<Individual> children = 
                new List<Individual>(this.PopulationSize + this.ChildSize);

            for (int i = 0; i < this.ChildSize; i++)
            {
                int position = GMHelper.Rand.Next(this.PopulationSize);
                Individual child = new Individual(this.Population[position]);
                Individual selectedIndividual = this.PropotionalSelect();

                this.Crossover(ref child, selectedIndividual);
                this.Mutation(ref child);

                if (selectedIndividual.Template.Cost <= this.BudgetLimit)
                {
                    this.FindFitness(ref child);
                    children.Add(child);
                }
            }

            children.AddRange(this.Population); // add last generation -- a form of elistism
            children.Sort(new FitnessComparer());
            children.RemoveRange(this.PopulationSize, children.Count); // remove excessive population -- Evolution Strategy

            this.Population = children;
        }
        private class FitnessComparer : IComparer<Individual>
        {
            #region IComparer<node> Members
            public int Compare(Individual x, Individual y)
            {
                return y.Fitness.CompareTo(x.Fitness);
            }
            #endregion
        }

        #endregion

        #region Get result

        /// <summary>
        /// Return new template which can win unit have template provided.
        /// Return null if not possible
        /// </summary>
        /// <returns></returns>
        public Template GetResult()
        {
            InitialPopulation();

            for (int i = 0; i < TotalTurns; i++)
            {
                this.NextTurn();

                if (this.Population.First().Fitness == 1) // can win all times already
                    return this.Population.First().Template;
            }

            this.Population.Sort(new FitnessComparer());

            if (this.Population.First().Fitness == 0)
                return null;

            float fitness = this.Population.First().Fitness;
            List<Individual> subset = (from p in Population
                                where p.Fitness > (fitness * 0.95f) // .95 is abitrary
                                select p).ToList();
            subset.Sort(new CostComparer());
            
            return subset.First().Template;
        }

        private class CostComparer:IComparer<Individual>
        {
            #region IComparer<node> Members

            public int Compare(Individual x, Individual y)
            {
                return y.Template.Cost.CompareTo(x.Template.Cost);
            }

            #endregion
        }
        #endregion
    }
}
